Skip to content

fix(graph): stable USAGE-edge ownership for directory-module nodes#844

Merged
DeusData merged 1 commit into
mainfrom
distill/828-usage-ownership
Jul 4, 2026
Merged

fix(graph): stable USAGE-edge ownership for directory-module nodes#844
DeusData merged 1 commit into
mainfrom
distill/828-usage-ownership

Conversation

@DeusData

@DeusData DeusData commented Jul 4, 2026

Copy link
Copy Markdown
Owner

fix(graph): stable USAGE-edge ownership for directory-module nodes

Distilled from #828 (author: Tommy Corbett spde89@gmail.com). The original
commits lacked DCO sign-offs and could not be salvaged by cherry-pick; the
content is applied verbatim plus one clang-format fix (graph_buffer.c — the
moved guard's strcmp condition joined to one line). Authorship is credited
via Co-authored-by.

Closes #787.

Problem

Since #667 switched Java/Go module QNs to directory-based QNs, every file in a
package shares one module QN — which also collides with the pipeline's
Folder/Project node for that directory. Class-level references (e.g.
private IRepository repo;) resolve their enclosing scope to that shared
directory node, so USAGE edges from all same-package files conflate into a
single source node whose file_path is whichever file was processed last
(sequential upsert) or whichever worker merged last (parallel merge) —
nondeterministic, varying run to run (#787).

Fix — three verified mechanisms

  1. cbm_gbuf_upsert_node (src/graph_buffer/graph_buffer.c) — the QA: reproduce-first bug suite + LSP/grammar extraction fixes (5-platform board green) #667
    guard only protected the structural node's label; a per-file Module def
    still clobbered its name/file_path/range/properties in place. The guard now
    skips the update entirely (early return existing->id;), protecting all
    fields of a Project/Folder node from Module-def collisions.
  2. merge_update_existing (src/graph_buffer/graph_buffer.c) — the
    parallel worker-gbuf merge applied unconditional "src wins", relabelling the
    Folder node to Module and setting its file_path to the last-merged worker's
    file. The same Module-on-container guard is applied; the ID remap stays
    outside the guard so worker edges are still redirected onto the
    canonical node.
  3. Source-node finders fall back to the per-file File node — new
    cbm_pipeline_node_is_dir_container() (src/pipeline/pipeline_internal.h);
    calls_find_source (pass_calls.c), find_source_node (pass_parallel.c) and
    find_enclosing_node (pass_usages.c) treat a lookup that lands on a
    Folder/Project node as a miss, so every file's class-level usages attribute
    to that file's unique File node.

Regression guard

tests/repro/repro_issue787.c (registered in Makefile.cbm + repro_main.c),
three tests on a minimal spring-petclinic analog (owner/IRepository.java +
owner/ServiceA.java + owner/ServiceB.java + web/WebController.java):

  • repro_issue787_usage_exact_sources — single sequential index: USAGE
    sources of IRepository must be exactly {ServiceA.java, ServiceB.java,
    WebController.java}.
  • repro_issue787_usage_stable_across_runs — 5 independent index rounds must
    return the identical, correct source set.
  • repro_issue787_usage_stable_parallel — same core fixture padded with 60
    filler classes (>50 files → parallel worker path), 3 rounds, exact set each
    round.

Red/green evidence

RED on unfixed origin/main (9871e7e) production sources, with the guard test
in place (0/3):

  repro_issue787_usage_exact_sources
    source count 2 != expected 3
      got: web/WebController.java
      got: owner/IRepository.java
  FAIL tests/repro/repro_issue787.c:355: USAGE edge source file_paths do not match expected set
  repro_issue787_usage_stable_across_runs
    source count 2 != expected 3
      got: web/WebController.java
      got: owner/IRepository.java
  FAIL tests/repro/repro_issue787.c:449: USAGE sources are stable but do not match expected set
  repro_issue787_usage_stable_parallel
    source count 2 != expected 3
      got: web/WebController.java
      got: owner/Filler34.java
    ^ parallel run 1
  FAIL tests/repro/repro_issue787.c:507: parallel-path USAGE sources wrong or unstable
[SUITE] repro_issue787                         0 passed, 3 failed

(The bogus owner/IRepository.java / owner/Filler34.java entries are the
last-writer-clobbered Folder-node file_path — the exact #787 symptom, in both
its sequential and parallel/race flavors.)

GREEN with the fix (3/3):

=== repro_issue787 ===
  repro_issue787_usage_exact_sources                     PASS
  repro_issue787_usage_stable_across_runs                PASS
  repro_issue787_usage_stable_parallel                   PASS
[SUITE] repro_issue787                         3 passed, 0 failed

Additional verification: make -f Makefile.cbm cbm (-Werror) clean;
make -f Makefile.cbm lint-ci (cppcheck + clang-format + NOLINT) green;
graph_buffer/pipeline/parallel/incremental/integration suites green
(471 passed).

Note: the repro tests were verified with CBM_INDEX_SUPERVISOR=0 (in-process
indexing). Without the kill switch, every rh_index_files-based repro test
on current main hangs: the #827 index supervisor spawns argv[0] cli --index-worker ..., and the repro runner selects suites via the inherited
CBM_REPRO_ONLY env (ignoring argv), so the worker child re-runs the repro
suite recursively. That is a pre-existing main-level issue introduced by #827,
independent of this fix — tracked separately.

Since #667 every file in a Java/Go package shares a directory-based module
QN that collides with the pipeline's Folder/Project node. Class-level
references resolved their enclosing scope to that shared node, whose
file_path was clobbered by each file's always-emitted Module def — last
writer wins (sequential upsert) or last-merged worker wins (parallel merge)
— so USAGE-edge sources conflated across same-package files and varied run
to run.

Three coordinated changes:
- cbm_gbuf_upsert_node: a Module def colliding with a Project/Folder node
  no longer updates ANY field (the #667 guard only protected the label).
- merge_update_existing: same guard on the parallel worker-gbuf merge path;
  the ID remap stays outside the guard so worker edges still redirect onto
  the canonical node.
- Source-node finders (pass_usages, pass_parallel, pass_calls) treat a
  lookup landing on a Folder/Project container as a miss and fall back to
  the per-file File node (new cbm_pipeline_node_is_dir_container helper).

Adds tests/repro/repro_issue787.c: exact-source-set, 5-run stability, and
>50-file parallel-path guards (red on the unfixed code in both sequential
and parallel flavors).

Distilled from #828: the author's commits lacked DCO sign-off trailers and
could not be cherry-picked; content applied verbatim plus one clang-format
fix in graph_buffer.c.

Closes #787.

Co-authored-by: Tommy Corbett <spde89@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData DeusData enabled auto-merge July 4, 2026 11:36
@DeusData DeusData merged commit 896aa91 into main Jul 4, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nondeterministic Java USAGE-edge misattribution since #667 — edges attach to wrong source files on every fresh index

1 participant